iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
自我挑戰組

入坑 RoR 必讀 - Ruby 物件導向設計實踐系列 第 14

Day14 CH7使用模組共用角色行爲(上)

  • 分享至 

  • xImage
  •  

理解角色

有些問題需要在其他不相關的物件之間共用行為。這種共同行為對類別來說是正確的 ,它是物件所扮演的角色。

在設計物件導向程式時,有些問題需要多個不相關的物件之間共享相似的行為,這種共同行為被視為角色,物件可以扮演這些角色。

當不相關的物件開始扮演相同的角色時,它們建立了一種依賴關係。這種關係不同於繼承的子類別/父類別關係,需要辨別這些角色,最小化他們之間的依賴關係。

何謂模組

還記得我們在第5章裡提到的鴨子類型嗎?鴨子類型Preparer就是一個角色。只要實作了Preparer介面的物件就扮演了這個角色。Mechanic類別、Tripcoordinator類別和Driver類別實作了prepare_trip方法,可以被視為Preparer,並與其他物件互動。

許多物件導向語言都提供了某種方式,可以定義一組被命名的方法。這些方法獨立於類別,並且可以被物件使用。在 Ruby裡,這種混入內容被稱為 模組 (module) ,然後這個模組可以加入到任何物件,模組裡的方法藉由自動委派能夠被物件使用。

我們可以從物件的角度來看,雖然跟繼承有點相似,但實際上的運作是如果物件接收到無法理解的訊息,那麼這些訊息會自動轉遞到其他地方;接著,正確的方法實作會被神奇地找到,然後執行,並傳回回應。

一個包含模組的物件可回應的所有訊息包括有:

  • 物件自身實作的訊息
  • 上游物件所實作的訊息
  • 被包含的模組所實作的訊息
  • 所有上游物件包含的模組所實作的訊息

組織化職責

當你遇到有許多相同的程式碼時,可以思考是否要建立鴨子類型,並將共用行為放入模組,而在這之前我們需要確認物件們的行為:

假設存在有一個Schedule類別,其介面已經包含了下面三個方法,每一個方法都帶有三個參數:實際目標,以及特定時間範圍的開始和結束日期。Schedule負責瞭解其傳入的target參數是否已被安排,並負責在排程表裡加入或移除target

scheduled?(target, starting, ending)
add(target, starting, ending)
remove(target, starting/ ending)

https://ithelp.ithome.com.tw/upload/images/20230914/20145409W5TieWidsm.jpg

  • Schedule自身會負責知道正確的前置時間。
  • schedulable?方法知道所有可能的值,並且它會檢查傳入target參數的類別,來決定該使用哪一種前置時間。

這個實作例子問題在於 Schedule都知道太多,這些資訊不該由Schedule提供,而應該屬於Schedule所檢查的類別。

移除依賴關係

  1. 建立Schedulable鴨子類別
    • 每一個方框代表一個類別
  • 將檢查類別的職責從schedulable?方法裡移除,並且會將lead_days訊息傳送給傳入的target參數。

  • Schedule類別不在乎target的類別,期望target能夠理解lead_days,也可以說是表現得像
    schedulable的事物。

    這項修改的目的是簡化程式碼,將責任推給最終物件,而不是對於類別的檢查。

    https://ithelp.ithome.com.tw/upload/images/20230914/20145409xvZRDNRYP6.jpg

  1. 讓物件自己說話

    假設有一個StringUtils類別,它實作了管理字串的實用方法。你可以向StringUtils傳送StringUtils.empty?(some_string)訊息,來詢問某個字串是否為空。

    物件應自我管理:

    • 物件應該包含自己的行為,自我管理。
    • 如果對B物件感興趣,不應被迫知道A物件,即使只是想要了解B物件的事情。

    圖 7.2 裡的那張順序圖違反了這項規則。發起者試圖確認target物件是否可調度。但它不會向target詢問 這個問題,實際上它問的是第三方(即Schedule),這樣做會迫使發起者知道並依賴於Schedule,這樣做會迫發起者知道並依賴於Schedule

撰寫具體的程式碼

先選擇一個具體的類別Bicycle,並在該類別裡面直接實作schedulable?方法,在做這項修改之前,每一個發起物件都必須知道Schedule,進而形成一段依賴關係 。

https://ithelp.ithome.com.tw/upload/images/20230914/20145409rxRD0hXsDw.jpg

class Schedule
	def scheduled?(schedulable, start_date, end_date) 
		puts "This #{schedulable.class} " +
			"is not scheduled\n" +
			" between #{start_date) and #{end_date}" 
		false
	end
end

Bicycle知道自己的調度前置時間,並且將schedulable?委派給了Schedule

class Bicycle
	attr_reader :schedule, :size, :chain, :tire_size
	
	# 注入 Schedule,並提供預設值
	def initialize(args = {})
		@schedule = args[:schedule] || Schedule.new 
		# ...
	end

	# 如果這輛自行車在(現在由 Bicycle 指定的)
	# 這段時間內可用就傳回真
	def scheedulable?(start_date, end_date)
		!scheduled?(start_date - lead_days, end_date )
	end

	# 傳回 schedule 的回應
	def scheduled?
		schedule.scheduled?(self, start_date, end_date)
	end
	
	# 傳回自行車可被調度前的
	# lead_days 數值

	def lead_days
		1
	end

	# ...
end

require 'date'
starting = Date.parse("2015/09/04")
ending = Date.parse("2015/09/10")

b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
#   between 2015-09-03 and 2015-09-10 
#  => true

這段程式碼將「Schedule是誰」以及「它做了什麼」的知識隱藏在Bicycle裡面。 持有Bicycle的物件不再需要知道Schedule的存在或者其行為。

擷取抽象

Bicycle並不是唯一「可調度」的。MechanicVehicle也都會扮演這個角色,因此它們也需要這個行為,現在需要重新安排這段程式碼,讓它能在不同類別之間共用。

module Schedulable
	attr_writer :schedule
	def schedule
		@schedule ||= ::Schedule.new
	end

	def schedulable?(start_date, end_date) 
		!scheduled?(start_date - lead_days, end_date)
	end

	def scheduled?(start_date, end_date) 
		schedule.scheduled?(seif, start_date, end_date)
	end

	# 包含者可以加以覆蓋
	def lead_days 
		0
	end
end

上面的範例展示了一個新的Schedulable模組。它包含一個從Bicycle擷取出來的抽象。

  1. 增加了一個schedule方法,對Schedule的依賴關係已從Bicycle裡移除,並且被移到Schedulable 模組,使其更加受到隔離。

  2. 之前Bicycle所實作的版本會傳 回針對自行車旳數值'現在,這個模組的實作版本會傳回一個更為普遍的預設值(即 0 天)。Schedulable模組也必須要實作lead_days方法。針對模組的規則與針對經典繼承的規則是一樣的。如果某個模組想要傳送訊息,它必須提供實作。lead_days方法是一個鉤子,它遵循範本方法模式。Bicycle則覆蓋了這個鉤子(第4行),來提供自己的特殊化。

class Bicycle
	include Schedulable

	def lead_days 
		1
	end

	#...
end

require 'date'
starting = Date.parse("2015/09/04") 
ending = rate.parse("2015/09/10")

b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
#   between 2015-09-03 and 2015-09-10 
#   => true

schedulable?訊息的傳送對象已從Bicycle轉變為Schedulable。現在你已經交給鴨子類型來處理 圖7.3則可以被調整成如圖7.4

https://ithelp.ithome.com.tw/upload/images/20230914/20145409LelpECEpBR.jpg

實作VehicleMechanic如何包括Schedulable模組 並且回應schedulable?訊息。

class Vehicle
	include Schedulable

	def lead_days 
		3
	end
	
	#...
end

class Mechanic 
	include Schedulable

	def lead_days
		4
	end

v = Vehicle.new
v.schedulable?(starting, ending)
# This Vehicle is not scheduled
#   between 2015-09-01 and 2015-09-10 
#   => true

m = Mechanic.new
m.schedulable?(starting, ending)
# This Mechanic is not scheduled
#   between 2015-08-31 and 2015-09-10 
#   => true

Schedulable裡的這段程式碼是抽象的,Schedulable覆蓋了lead_days,當schedulable?抵達任何 Schedulable時,該訊息會自動委派給這個模組中所定義的方法。

這個舉例可能並不符合嚴格的經典繼承定義,但撰寫技巧其實都是一樣的,因為都是沿著相同的路徑去尋找可 用方法。

今天又講了好大一篇,明天再接續瞭解是如何尋找相對應的方法,以及他的脈絡吧!

參考資料:

  • Practical Object-Oriented Design in Ruby: An Agile Primer

上一篇
Day13 CH6 藉由繼承取得行為(下)
下一篇
Day15 CH7使用模組共用角色行爲(下)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言